package creation

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/nuttech/bell"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/kubeapi"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	"gitverse.ru/synapse/kubelatte/pkg/operator"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"sigs.k8s.io/yaml"
)

type FactoryController struct {
	Client   *kubernetes.Clientset
	chObject chan bell.Message
	chStop   chan bool
}

func (f *FactoryController) SetClient(client *kubernetes.Clientset) {
	f.Client = client
}

func (f *FactoryController) make(message bell.Message) {
	tiObject := message.Value.(v1alpha1.TriggerInstance)
	logs.Infof("FactoryController: make %s", tiObject.GetNamespacedName())

	ctx := context.Background()

	if len(tiObject.Status.ResourceStatus) > 0 {
		// resources were created only check status and watch
		allRemoved := true
		for _, resource := range tiObject.Status.ResourceStatus {
			if resource.Status != operator.FactoryResourceRemoved {
				allRemoved = false
			}
		}
		if allRemoved {
			logs.Infof("FactoryController: All resources removed for ti %s, ignore instance", tiObject.GetNamespacedName())
			return
		}
		// one or more resources are alive so keep watching
	} else {
		// resources were not created, full logic
		RC.CheckTriggerTemplatePair(ctx) // Validate current configuration
		if len(tiObject.Spec.Data) > 0 {
			var dataObj map[string]interface{}
			err := yaml.Unmarshal([]byte(tiObject.Spec.Data), &dataObj)
			if err != nil {
				logs.Error(err.Error())
				return
			}
			tiObject.Spec.DataObj = dataObj
		}

		renders := RC.GetRenders(ctx, &tiObject)
		for _, templateRender := range renders {
			err := f.createResource(ctx, &tiObject, templateRender)
			if err != nil {
				logs.Errorf(err.Error())
			}
		}
	}

	err := util.SendEventWithErr(operator.WatcherAddNewObjectEvent, tiObject)
	if err != nil {
		logs.Error(fmt.Sprintf("FactoryController: ti %s failed to create watch event: %s", tiObject.GetNamespacedName(), err.Error()))
		return
	}
}

func (f *FactoryController) checkIfProcessed(name string, ti *v1alpha1.TriggerInstance, checkIsRemoved bool) bool {
	for i := 0; i < len(ti.Status.ResourceStatus); i++ {
		if ti.Status.ResourceStatus[i].Name == name {
			if checkIsRemoved && ti.Status.ResourceStatus[i].Status == operator.FactoryResourceRemoved {
				return true
			} else if !checkIsRemoved {
				return true
			}
		}
	}
	return false
}

func (f *FactoryController) Start() {
	f.SetClient(kubeapi.GetClient())
	f.init()
	logs.Debugf("FactoryController: Start")
}

func (f *FactoryController) Stop() {
	f.chStop <- true
	close(f.chObject)
	close(f.chStop)
	bell.Remove(operator.FactoryCreateRequestEvent)
	logs.Debugf("FactoryController: Stop")
}

func (f *FactoryController) getResourceName(tiName, kind string, render util.RenderItem) (string, error) {
	resourceName := tiName

	if render.CreationIsUniqueName {
		resourceName = fmt.Sprintf("%s-%s", tiName, operator.GetShortHash(operator.GetRandomPart()+render.Template.Name+kind))
	} else {
		var template map[string]interface{}
		err := yaml.Unmarshal([]byte(render.Render), &template)
		if err != nil {
			return "", err
		}

		metadata, ok := template["metadata"]
		if ok && metadata != nil {
			name, ok := metadata.(map[string]interface{})["name"].(string)
			if ok {
				resourceName = name
			}
		}
	}

	return resourceName, nil
}

func (f *FactoryController) getResourceFromKubeApi(ctx context.Context, kind, apiVersion, name, namespace string) ([]byte, error) {
	resourceMeta := kubeapi.GetResourceMeta(f.Client, kind, apiVersion)
	if resourceMeta == nil {
		return nil, fmt.Errorf("failed to get meta for resource : ns %s; apiVersion %s; kind %s ", namespace, apiVersion, kind)
	}

	res, err := kubeapi.GetResource(ctx, resourceMeta, name, namespace)
	return res, err
}

func (f *FactoryController) createResource(ctx context.Context, tiObject *v1alpha1.TriggerInstance, render util.RenderItem) error {
	kind := render.Template.Spec.Kind
	version := render.Template.Spec.ApiVersion

	resourceName, err := f.getResourceName(tiObject.Name, kind, render)
	if err != nil {
		return f.returnErrorWithUpdateStatus(ctx, tiObject,
			version, kind, resourceName,
			fmt.Errorf("FactoryController: failed to get name for resource : err %s, version %s, r %s ", err.Error(), version, kind),
		)
	}

	namespaceName := tiObject.Namespace
	_, err = f.getResourceFromKubeApi(ctx, kind, version, resourceName, namespaceName)
	if err != nil {
		if !errors.IsNotFound(err) {
			return f.returnErrorWithUpdateStatus(ctx, tiObject,
				version, kind, resourceName,
				fmt.Errorf("FactoryController: failed to check resource in: ns %s v %s r %s error %s", namespaceName, version, kind, err.Error()),
			)
		}
	} else {
		// resource exist so we can not move next
		logs.Error(fmt.Sprintf("FactoryController: Resource %s kind %s exist in ns %s, fail creation", resourceName, kind, namespaceName))
		return nil
	}

	template, err := f.modifyTemplate(ctx, tiObject, render, version, kind, resourceName, namespaceName)
	if err != nil {
		return err
	}

	logs.Debug(fmt.Sprintf("FactoryController: Resource %s kind %s \nbody %v", resourceName, kind, template))

	if f.checkIfProcessed(resourceName, tiObject, false) {
		logs.Warn(fmt.Sprintf("FactoryController: Object %s already processed", resourceName))
		return nil
	}

	resourceMeta := kubeapi.GetResourceMeta(f.Client, kind, version)
	if resourceMeta == nil {
		return f.returnErrorWithUpdateStatus(ctx, tiObject,
			version, kind, resourceName,
			fmt.Errorf("FactoryController: failed to get meta for resource : ns %s v %s r %s ", namespaceName, version, kind),
		)
	}

	jsBody, err := json.Marshal(template)

	var object map[string]interface{}
	raw, err, fullErr := kubeapi.CreateResourceInKubeApi(ctx, f.Client.RESTClient(), resourceMeta, jsBody, namespaceName)

	logs.Debug(fmt.Sprintf("FactoryController: RAW Resource %s kind %s \nbody %v", resourceName, kind, string(raw)))

	if err != nil {
		resourceMeta = kubeapi.InvalidateCacheAndTryGetResourceMeta(f.Client, kind, version)

		if resourceMeta == nil {
			return f.returnErrorWithUpdateStatus(ctx, tiObject,
				version, kind, resourceName,
				fmt.Errorf("FactoryController: failed to get meta for resource : ns %s v %s r %s ", namespaceName, version, kind),
			)
		}

		raw, err, fullErr = kubeapi.CreateResourceInKubeApi(ctx, f.Client.RESTClient(), resourceMeta, jsBody, namespaceName)
		logs.Debug(fmt.Sprintf("FactoryController: Updated RAW Resource %s kind %s \nbody %v", resourceName, kind, string(raw)))
		if err != nil {
			return f.returnErrorWithUpdateStatus(ctx, tiObject,
				version, kind, resourceName,
				fmt.Errorf("FactoryController: Create resource failed in: ns %s v %s r %s error %s", namespaceName, version, kind, fullErr),
			)
		}
	}
	err = json.Unmarshal(raw, &object)
	if err != nil {
		return f.returnErrorWithUpdateStatus(ctx, tiObject,
			version, kind, resourceName,
			fmt.Errorf("FactoryController: Unmarshal resource failed in: ns %s v %s r %s error %s", namespaceName, version, kind, err.Error()),
		)
	}

	var status map[string]interface{}
	phase := "Apply"

	status, ok := object["status"].(map[string]interface{})
	if ok {
		phs, ok := status["phase"]
		if ok {
			phase = phs.(string)
		}
	}

	data, err := yaml.Marshal(status)
	if err != nil {
		logs.Error("FactoryController: Update trigger status failed: " + err.Error())
	}

	f.updateTIResourceStatus(ctx, tiObject, resourceName, "", phase, string(data), kind, version)

	return nil
}

func (f *FactoryController) modifyTemplate(ctx context.Context, tiObject *v1alpha1.TriggerInstance, render util.RenderItem, version string, kind string, resourceName string, namespaceName string) (map[string]interface{}, error) {
	var template map[string]interface{}
	err := yaml.Unmarshal([]byte(render.Render), &template)
	if err != nil {
		return nil, fmt.Errorf("FactoryController: failed to unmarshal template resource in: ns %s v %s r %s error %s", namespaceName, version, kind, err.Error())
	}

	metadata, ok := template["metadata"]
	if !ok || metadata == nil {
		template["metadata"] = make(map[string]interface{})
		metadata = template["metadata"]
	}

	metadata.(map[string]interface{})["name"] = resourceName
	metadata.(map[string]interface{})["namespace"] = namespaceName
	metadata.(map[string]interface{})["ownerReferences"] = []map[string]string{
		0: {
			"apiVersion": tiObject.APIVersion,
			"kind":       tiObject.Kind,
			"name":       tiObject.Name,
			"uid":        string(tiObject.UID),
		},
	}

	labels, ok := metadata.(map[string]interface{})["labels"]
	if !ok || labels == nil {
		template["metadata"].(map[string]interface{})["labels"] = make(map[string]interface{})
		labels = template["metadata"].(map[string]interface{})["labels"]
	}

	labels.(map[string]interface{})[operator.KbltCreatorLabelKey] = operator.KbltCreatorLabelValue
	if tiObject.Labels != nil {
		for k, v := range tiObject.Labels {
			labels.(map[string]interface{})[k] = v
		}
	}

	annotations, ok := metadata.(map[string]interface{})["annotations"]
	if !ok || annotations == nil {
		template["metadata"].(map[string]interface{})["annotations"] = make(map[string]interface{})
		annotations = template["metadata"].(map[string]interface{})["annotations"]
	}
	if tiObject.Annotations != nil {
		for k, v := range tiObject.Annotations {
			annotations.(map[string]interface{})[k] = v
		}
	}
	return template, err
}

func (f *FactoryController) returnErrorWithUpdateStatus(ctx context.Context, tiObject *v1alpha1.TriggerInstance, version string, kind string, resourceName string, err error) error {
	f.updateTIResourceStatus(ctx, tiObject, resourceName, err.Error(), operator.FactoryResourceCreateFailed, "", kind, version)
	return err
}

func (f *FactoryController) updateTIResourceStatus(ctx context.Context, tiObject *v1alpha1.TriggerInstance, resourceName string, message string, phase string, data string, kind string, version string) {
	tiActualObject, err := operator.Cl.TriggerInstance().Get(ctx, tiObject.Namespace, tiObject.Name, metav1.GetOptions{})
	if err != nil {
		logs.Error("FactoryController: tiActualObject cant get: " + err.Error())
		return
	}
	tiActualObject.UpdateResourceStatus(resourceName, message, phase, data, kind, version)
	updatedTI, err := operator.Cl.TriggerInstance().UpdateStatus(ctx, tiObject.Namespace, tiActualObject, metav1.UpdateOptions{})
	if err != nil {
		logs.Error("FactoryController: failed to updateTIResourceStatus: " + err.Error())
		return
	}
	tiObject = updatedTI
}

func (f *FactoryController) init() {
	f.chObject = make(chan bell.Message)
	f.chStop = make(chan bool)
	bell.Listen(operator.FactoryCreateRequestEvent, f.addNewObject)
	go f.runProcessLoop()
}

func (f *FactoryController) addNewObject(message bell.Message) {
	f.chObject <- message
}

func (f *FactoryController) runProcessLoop() {
	for {
		select {
		case v := <-f.chObject:
			go f.make(v)
		case <-f.chStop:
			return
		}
	}
}
